package AnnBob;

import Util.FolderCreator;

import java.util.HashSet;
import java.util.LinkedList;
import java.io.*;
import java.math.BigInteger;
import java.nio.channels.FileLock;

public class Cryptography {
    private final static String    MAIN_DIR    = ".\\main\\";
    private final static String    CIPHER_DIR  = ".\\cipher text\\";
    private final static String    PLAIN_DIR   = ".\\plain text\\";
    private final static String    KEYS_DIR   = ".\\keys\\";
    
    private final static char      START_SYMBOL = 'a';
    private final static char      END_SYMBOL = 'z';
    private final static int       N = END_SYMBOL - START_SYMBOL + 1;

    public static void main(String[] args){
        System.out.println("AnnBob started.");        
        boolean rsa = args.length != 0 && (args[0].equals("secure"));
        if (rsa){
            System.out.println("Secure keys exchange (bonus A) enabled.");
        }
        else{
            System.out.println("Secure keys exchange (bonus A) disabled, to enable use:");
            System.out.println("java -jar AnnBob.jar secure");
        }
        Cryptography c= new Cryptography(rsa);
        c.run();
    }    


    // Object region

    private HashSet<String> m_processedFiles;
    private char[] m_key;
    private RSA key;  //RSA key for encrypt the swap ky
    private final boolean RSA_ENABLED;
    
    public Cryptography(boolean rsa) {
        FolderCreator.createFolders();
        RSA_ENABLED = rsa;
        this.m_processedFiles= new HashSet<String>();
        m_key = createKey();
        if (RSA_ENABLED)
            key = new RSA(N*16 + 1);
    }

    /**
     * check if new files were added, if so than act acoording to the file type
     */
    public void run() {
        //noinspection InfiniteLoopStatement
        while(true){
            LinkedList<String> newFiles = getNewFiles(MAIN_DIR);
            for(String filename:newFiles){
                try {
                    handleFile(filename);                    
                }
                catch (IOException e) {
                    m_processedFiles.remove(filename);
                    System.out.println("Thecnical problem to access "+filename+", will retry later");
                }
            }
            //noinspection EmptyCatchBlock
            try {
                Thread.sleep(1000);
            }
            catch (InterruptedException e) {}
        }
    }

    /**
     * get the new files in the directory
     * @param directoryName     The directory to check
     * @return linked list of the new files inserted to the directory
     */
    private LinkedList<String> getNewFiles(String directoryName){
        LinkedList<String> ret= new LinkedList<String>();
        File dir = new File(directoryName);
        String[] children = dir.list();
        if (children != null) {
            for (String filename : children) {
                // Get filename of file or directory
                if (!this.m_processedFiles.contains(filename)) {
                    ret.add(filename);
                    m_processedFiles.add(filename);
                }
            }
        }
        return ret;
    }

    /**
     * creat key of the a-z letters by swap method
     * @return  A random key
     */
    private static char[] createKey(){
        char[] key=new char[N];
        for (int i=0; i<N; i++)
            key[i] = (char) (i+START_SYMBOL);
        // shuffle
        for (int i = 0; i < N; i++) {
            int r = (int) (Math.random() * (i+1));     // int between 0 and i
            char swap = key[r];
            key[r] = key[i];
            key[i] = swap;
        }
        return key;
    }

    /**
     * in case new file was added- handale it according to it's type
     * @param filename       The file to handle.
     * @throws IOException   A problem accessing the file
     */
    private void handleFile(String filename) throws IOException {
        //all the new files are in the main dir
        String name;
        System.out.println("Processing file: " + filename);
        if(filename.endsWith(".txt")){
            name= filename.substring(0,filename.length()-4);
            encrypt(name);
            saveKey(name);
            System.out.println("Done encrypting file");
        }
        if(filename.endsWith(".ct")){
            name= filename.substring(0,filename.length()-3);
            decrypt(name);
            System.out.println("Done decrypting file");
        }        
    }

    /**
     * saves the key (encrypted or not) in the keys directory
     * @param filename          The filename to save the key
     * @throws IOException      A problem when saving the key
     */
    private void saveKey(String filename) throws IOException {
        byte[] message = new byte[2*N];
        for (int i=0; i<N; i++){
            message[2*i] = (byte)(m_key[i]);
            message[2*i+1] = '\n';
        }

        if (RSA_ENABLED){
            // create message by converting string to integer
            BigInteger encrypt = key.encrypt(new BigInteger(message));
            message = encrypt.toByteArray();
        }

        FileOutputStream fos = new FileOutputStream(KEYS_DIR+"\\"+filename+".key");
        FileLock fosLock = fos.getChannel().lock();
        fos.write(message);
        fosLock.release();
        fos.close();
    }

    /**
     * takes the right key and decrypt it if nessasary
     * @param filename      The filename to load the key
     * @return the decrypted key
     * @throws IOException  A problem when loading the key
     */
    private char[] loadKey(String filename) throws IOException {        
        FileInputStream fis = new FileInputStream(KEYS_DIR+"\\"+filename+".key");
        FileLock fisLock = fis.getChannel().lock(0l,Long.MAX_VALUE,true);
        byte[] message = new byte[2*N];
        //noinspection ResultOfMethodCallIgnored
        fis.read(message,0,2*N);
        fisLock.release();
        fis.close();
        
        if (RSA_ENABLED){
            // create message by converting string to integer
            BigInteger decrypt = key.decrypt(new BigInteger(message));
            message = decrypt.toByteArray();
        }
                
        return (new String(message)).replaceAll("\n","").toCharArray();
    }

    /**
     *  encrypt the plain text with the swap key
     * @param filename          The filename to encrypt
     * @throws IOException      A problem accessing the file
     */
    private void encrypt(String filename) throws IOException {
        FileInputStream fis;
        try {
            fis = new FileInputStream(MAIN_DIR+"\\"+filename+".txt");
        }
        catch (FileNotFoundException e) {
            m_processedFiles.remove(filename+".txt");
            return;
        }
        FileOutputStream fos = new FileOutputStream(CIPHER_DIR+"\\"+filename+".ct");
        FileLock fisLock = fis.getChannel().lock(0l,Long.MAX_VALUE,true);
        FileLock fosLock = fos.getChannel().lock();
        
        int c=fis.read();
        while (c!=-1){
            if(c<START_SYMBOL || c>END_SYMBOL)
                fos.write((char)c);
            else
                fos.write(m_key[c-START_SYMBOL]);

            c=fis.read();
        }
        
        fisLock.release();
        fosLock.release();
        fis.close();
        fos.close();
    }
    /**
     *  decrypt the cipher text using the swap key
     * @param filename          The filename to decrypt
     * @throws IOException      A problem accessing the file
     */
    private void decrypt(String filename) throws IOException {
        // Get a file channel for the file
        FileInputStream fis;
        try {
            fis = new FileInputStream(MAIN_DIR+"\\"+filename+".ct");
        }
        catch (FileNotFoundException e) {
            m_processedFiles.remove(filename+".ct");
            return;
        }
        FileOutputStream fos = new FileOutputStream(PLAIN_DIR+"\\"+filename+".txt");
        FileLock fisLock = fis.getChannel().lock(0l,Long.MAX_VALUE,true);
        FileLock fosLock = fos.getChannel().lock();      

        char[] decrKey = loadKey(filename);
        char[] invertedKey = invertKey(decrKey);
        int c=fis.read();
        while (c!=-1){
            if(c<START_SYMBOL || c>END_SYMBOL)
                fos.write((char)c);
            else{
                fos.write(invertedKey[c-START_SYMBOL]);
            }
            c=fis.read();
        }

        fisLock.release();
        fosLock.release();
        fis.close();
        fos.close();
        File f = new File(MAIN_DIR+"\\"+filename+".ct");
        f.delete();
    }

    private static char[] invertKey(char[] key) {
        char[] res = new char[N];
        for (int i=0; i<N; i++) {
            res[key[i]-START_SYMBOL] = (char) (i+START_SYMBOL);
        }
        return res;
    }
}
